The “useImmer” Hook
So, hopefully you agree that Immer is pretty darn cool. Especially when working with complex / deeply-nested data, it's often so much simpler to use mutation. Immer gives us the best of both worlds.
But it does come at a price. Reducers are already quite boilerplate-heavy, and Immer adds even more:
function reducer(state, action) { return produce(state, (draftState) => { switch (action.type) { ... } });}
That's a lot of stuff to type out whenever we need a reducer!
Now, personally, I don't mind this. As I mentioned recently, I find it helps build momentum, to get the ball rolling. But a lot of developers find it irksome, to have to write this chunk of scaffolding every time they need a reducer.
The Immer team has released an official NPM package called use-immer. It includes a custom hook called useImmerReducer
which automatically applies the produce
function.
It looks like this:
import React from 'react';import { useImmerReducer } from 'use-immer';
const initialState = { count: 0 };
function reducer(draftState, action) { switch (action.type) { case 'increment': { draftState.count++; return; }
case 'decrement': { draftState.count--; return; }
case 'reset': { return initialState; } }}
function Counter() { const [state, dispatch] = useImmerReducer(reducer, initialState);
return ( <> Count: {state.count} <button onClick={() => dispatch({ type: 'increment' })}> Increment </button> <button onClick={() => dispatch({ type: 'decrement' })}> Decrement </button> <button onClick={() => dispatch({ type: 'reset' })}> Reset </button> </> );}
Within this reducer
function, we're never given the original state, only the draftState
created by the produce
function.
Essentially, it allows us to turn this:
function reducer(state, action) { return produce(state, (draftState) => { switch (action.type) { ... } });}
…into this:
function reducer(draftState, action) { switch (action.type) { ... }}
The same package also includes a useImmer
hook, which is essentially an immer-wrapped useState
alternative:
import React from 'react';import { useImmer } from 'use-immer';
function App() { const [person, updatePerson] = useImmer({ name: 'Michel', age: 33 });
function becomeOlder() { updatePerson((draft) => { draft.age++; }); }
return ( <div className="App"> <h1> Hello {person.name} ({person.age}) </h1>
<button onClick={becomeOlder}>Older</button> </div> );}
Now, I don't personally use this package. Immer's boilerplate doesn't bug me, and it worries me a little that this package makes Immer so implicit. You can't tell from looking at the reducer
function whether we're allowed to mutate the state or not (aside from the fact that we've named the parameter draftState
). I want to be super, super explicit when it comes to using Immer, because I don't want newer developers to be confused about when they are/aren't allowed to mutate things.
That said, I suspect I'm in the minority. A lot of developers really benefit from this package. So I wanted to share it with you, in case you found it useful!
You can learn more about this package over on Github. Thanks to Discord user Nachos for reminding me that this package exists!